#!/usr/bin/env node

/* == KiriMotoSlicer ==
 * Command-Line Interface (CLI) for Kiri:Moto web framework
 *    written by Rene K. Mueller, based on cli.js by Stewart Allen
 *
 * History:
 * 2021/09/27: 0.0.7: -l supports also JSON of format as in src/dev/*
 * 2021/09/01: 0.0.6: retrieving version fixed (from license.js), mute kiri startup
 * 2021/08/16: 0.0.5: renamed KiriMotoSlicer to kirimoto-slicer
 * 2021/08/08: 0.0.4: -h <term> or --help=<term> to query settings
 * 2021/08/08: 0.0.3: supporting --bedOrigin=<x>,<y> use with --outputOriginCenter=true
 * 2021/08/07: 0.0.2: trying to make it standalone (what a mess NodeJS/js is)
 * 2021/08/02: 0.0.1: starting based on cli.js by Stewart Allen, processing switches, load_conf()
 */

// 'use strict'   // Note: we can't use 'strict' as some code should behave like in a browser :-| => lazy strictness

let APPNAME = 'KiriMotoSlicer';
let VERSION = '0.0.7';
let BINNAME = 'kirimoto-slicer';

let v = process.version.match(/^v(\d+\.)/)[1];
v = parseFloat(v);
if(v<=12) {
   console.log(`ERR: node version ${ver} is not sufficient to run ${APPNAME}, at least use 13.0 or later, abort.`);
   process.exit(-1);
}
let conf = { 
   verbose: 0,
   basedir: "/usr/local/lib/kirimoto",
   setTemperature: true,
   bedOrigin: [ 0, 0 ]
};

let s2l = { v: 'verbose', o: 'output', l: 'load', h: 'help' };
let a2a = { o: 1, l: 1, h: 1 };
let fss = [];

let fs = require('fs');
let args = process.argv.slice(2);
//let dir = args[0] || ".";
let dir = conf.basedir + "/grid-apps" || ".";

let conf_options = {
   sliceFillType: [ "vase", "hex", "grid", "gyroid", "triangle", "linear", "bubbles" ],
   sliceShellOrder: [ "in-out", "out-in" ],
   sliceLayerStart: [ "last", "center", "origin" ],
   mode: [ 'FDM', 'SLA', 'CNC', 'Laser' ],
};

let slice_conf = {
  "processName":"generic",
  "sliceHeight":0.25,
  "sliceShells":3,
  "sliceShellOrder":"in-out",
  "sliceLayerStart":"last",
  "sliceFillAngle":45,
  "sliceFillOverlap":0.3,
  "sliceFillSparse":0.2,
  "sliceFillType":"gyroid",
  "sliceAdaptive":false,
  "sliceMinHeight":0,
  "sliceSupportDensity":0.25,
  "sliceSupportOffset":0.4,
  "sliceSupportGap":1,
  "sliceSupportSize":6,
  "sliceSupportArea":1,
  "sliceSupportExtra":0,
  "sliceSupportAngle":50,
  "sliceSupportNozzle":0,
  "sliceSolidMinArea":10,
  "sliceSolidLayers":3,
  "sliceBottomLayers":3,
  "sliceTopLayers":3,
  "firstLayerRate":10,
  "firstLayerPrintMult":1.15,
  "firstLayerYOffset":0,
  "firstLayerBrim":0,
  "firstLayerBeltLead":3,
  "sliceSkirtCount": 0,
  "sliceSkirtOffset": 2,
  "sliceLineWidth": 0,
  "outputTemp":210,
  "outputBedTemp":60,
  "outputFeedrate":50,
  "outputFinishrate":50,
  "outputSeekrate":80,
  "outputShellMult":1.25,
  "outputFillMult":1.25,
  "outputSparseMult":1.25,
  "outputRetractDist":4,
  "outputRetractSpeed":30,
  "outputRetractDwell":30,
  "outputShortPoly":100,
  "outputMinSpeed":10,
  "outputCoastDist":0.1,
  "outputLayerRetract":true,
  "detectThinWalls":true,
  "zHopDistance":0,
  "antiBacklash":0,
  "outputOriginCenter":false,
  //"setOriginCenter": [ 50, 50 ],
  "sliceFillRate":0,
  "sliceSupportEnable":false,
  "firstSliceHeight":0.25,
  "firstLayerFillRate":35,
  "firstLayerLineMult":1,
  "firstLayerNozzleTemp":0,
  "firstLayerBedTemp":0,
  "firstLayerBrimTrig":0,
  "outputRaft":false,
  "outputRaftSpacing":0.2,
  "outputRetractWipe":0,
  "outputBrimCount":2,
  "outputBrimOffset":2,
  "outputLoopLayers":null,
  "outputInvertX":false,
  "outputInvertY":false,
  "arcTolerance":0,
  "gcodePause":"",
  "ranges":[],
  "firstLayerFanSpeed":0,
  "outputFanSpeed":255
}
let device_conf = {
  "noclone":false,
  "mode":"FDM",
  "internal":0,
  "imageURL":"",
  "imageScale":0.75,
  "imageAnchor":0,
  "bedHeight":2.5,
  "bedWidth":220,
  "bedDepth":220,
  "bedRound":false,
  "bedBelt":false,
  "maxHeight":300,
  "originCenter":false,
  "extrudeAbs":true,
  "spindleMax":0,
  "gcodeFan":[ "M106 S{fan_speed}" ],
  "gcodeTrack":[],
  "gcodeLayer":[";LAYER:{layer}"],     // make it cura/slic3r compatible
  "gcodePre":[
  "M107                     ; turn off filament cooling fan",
  "G90                      ; set absolute positioning mode",
  "M82                      ; set absolute positioning for extruder",
  "M104 S{temp} T{tool}     ; set extruder temperature",
  "M140 S{bed_temp} T{tool} ; set bed temperature",
  "G28                      ; home axes",
  "G92 X0 Y0 Z0 E0          ; reset all axes positions",
  "G1 X0 Y0 Z0.25 F180      ; move XY to 0,0 and Z 0.25mm over bed",
  "G92 E0                   ; zero the extruded",
  "M190 S{bed_temp} T{tool} ; wait for bed to reach target temp",
  "M109 S{temp} T{tool}     ; wait for extruder to reach target temp",
  "G92 E0                   ; zero the extruded",
  "G1 F225                  ; set feed speed"
  ],
  "gcodePost":[
  "M107                     ; turn off filament cooling fan",
  "M104 S0 T{tool}          ; turn off right extruder",
  "M140 S0 T{tool}          ; turn off bed",
  "G1 X0 Y300 F1200         ; end move",
  "M84                      ; disable stepper motors"
  ],
  "gcodeProc":"",
  "gcodePause":[],
  "gcodeDwell":[],
  "gcodeSpindle":[],
  "gcodeChange":[],
  "gcodeFExt":"gcode",
  "gcodeSpace":true,
  "gcodeStrip":false,
  "gcodeLaserOn":[],
  "gcodeLaserOff":[],
  "extruders":[
  {
      "extFilament":1.75,
      "extNozzle":0.4,
      "extSelect":[ "T0" ],
      "extDeselect":[],
      "extOffsetX":0,
      "extOffsetY":0
  }
  ],
  "new":false,
  "deviceName":"3D Printer"
};

for(var i=0; i<args.length; i++) {              // parse arguments
   let a = args[i]
   let m
   m = a.match(/^--([\w\-\.]+)$/)                // --<k>
   if(m && m[1]) {
      let a = m[1]
      if(typeof conf[a] === "undefined")
         conf[a] = 0
      conf[a]++
      continue
   }
   m = a.match(/^-([\w]+)$/);                    // -<k>
   if(m && m[1]) {
      for(k of m[1].split('')) {
         let kf = s2l[k]
         if(typeof kf === "undefined") {
            console.log(`WARN: unknown short switch '${k}', ignored`);
            continue
         }
         if(typeof conf[kf] === "undefined" && a2a[k] === "undefined")    // not already set and no additional argument?
            conf[kf] = 0
         if(typeof a2a[k] !== "undefined")      // short key takes additional argument?
            if(kf == 'load')
               load_conf(args[++i])
            else
               conf[kf] = args[++i]
         else
            conf[kf]++
      }
      continue
   }
   m = a.match(/^--([\w\-\.]+)=(.*)/)          // --<k>=<v>
   if(m && m[1]) {                            // don't test m[2] as it can be empty
      let k = m[1]
      let v = m[2]
      if(k == 'load') 
         load_conf(v)
      else
         conf[k] = Array.isArray(conf[k]) ? v.split(',').map(function(s) { return parseInt(s)}) : v
      continue
   } 
   m = a.match(/^-/)
   if(m) {
      console.log(`ERR: mal-formed switch: '${a}'`)
      help()
   }
   if(false && !fs.existsSync(a))
      console.log(`ERR: unknown switch/option or non-accessible file: '${a}', skipped.`)
   else
      fss.push(a)                               // no switch, then consider it as file to process
}

if(!fs.existsSync(conf.basedir)) {
   console.log(`ERR: basedir doesn't exist: ${conf.basedir}, do proper 'make install', abort.`);
   process.exit(-1);
} 

if(conf.help && (conf.help!=1 && conf.help.length > 0)) {
   let re = RegExp(conf.help,"i");
   console.log(`query settings for '${conf.help}':`)
   let n = 0;
   for(let s of [slice_conf, device_conf]) {
      for(let k in s) {
         if(k.match(re)) {
            list_option(k,s);
            n++;
         }
      }
   }
   if(n==0)
      console.log("nothing found")
   process.exit(0);
}

// post process configs: split into slice_conf and device_conf
for(let k in conf) {
   if(k.match(/\./)) {                                    // deep setting
      if(typeof dot(slice_conf,k) !== 'undefined') 
         dot(slice_conf,k,conf[k])
      else //if(typeof dot(device_conf,k) !== 'undefined') 
         dot(device_conf,k,conf[k])
   } else if(typeof slice_conf[k] !== 'undefined') {
      if(Array.isArray(slice_conf[k])) 
         slice_conf[k] = conf[k].split(/\s*,\s*/);
      else 
         slice_conf[k] = typeof slice_conf[k] == "number" ? parseFloat(conf[k]) : 
            typeof slice_conf[k] == "boolean" ? (conf[k]=="true" || parseInt(conf[k])==1 ? true : false) : conf[k];
      //console.log(k,conf[k],parseInt(conf[k]),JSON.stringify(slice_conf[k]),typeof slice_conf[k])
   } else if(typeof device_conf[k] !== 'undefined') {
      if(Array.isArray(device_conf[k])) {
         conf[k] = conf[k].replace(/\\n/g,"\n");
         device_conf[k] = conf[k].split(/\n/);
      } else
         device_conf[k] = typeof device_conf[k] == "number" ? parseFloat(conf[k]) : 
            typeof device_conf[k] == "boolean" ? (conf[k]=="true" || parseInt(conf[k])==1 ? true : false) : conf[k];
      //console.log(k,conf[k],parseInt(conf[k]),JSON.stringify(device_conf[k]))
   } else if(conf.verbose)
      console.log(`WARN: setting '${k}' doesn't fit to slice or device configuration`);
}

if(conf.setTemperature) {
   device_conf.gcodePre.push("\n; set temperatures",
      "M104 S{temp} T{tool}     ; set extruder temperature",
      (slice_conf.outputBedTemp > 0 ? "M140 S{bed_temp} T{tool} ; set bed temperature" : "" ),
      (slice_conf.outputBedTemp > 0 ? "M190 S{bed_temp} T{tool} ; wait bed temperature" : "" ),
      "M109 S{temp} T{tool}     ; wait extruder temperature",
   );
}
device_conf.gcodePre.push("G92 E0");      // making sure we start from E0

if(conf.verbose)
   console.log({slice_conf:slice_conf, device_conf:device_conf})
   
// -- load kiri:moto slicer (based on cli.js code: pretend to be a browser, and load all what is required)
//console.log(dir);
let files = [
    "kiri",
    "ext/three",
    "ext/pngjs",
    "ext/jszip",
    "license",
    "ext/clip2",
    "ext/earcut",
    "add/array",
    "add/three",
    "add/class",
    "geo/base",
    "geo/debug",
    "geo/point",
    "geo/points",
    "geo/slope",
    "geo/line",
    "geo/bounds",
    "geo/polygon",
    "geo/polygons",
    "geo/gyroid",
    "moto/pack",
    "kiri/conf",
    "kiri/client",
    "kiri/engine",
    "kiri/slice",
    "kiri/slicer",
    "kiri/slicer2",
    "kiri/layers",
    "mode/fdm/fill",
    "mode/fdm/driver",
    "mode/fdm/slice",
    "mode/fdm/prepare",
    "mode/fdm/export",
    "mode/sla/driver",
    "mode/sla/slice",
    "mode/sla/export",
    "mode/cam/driver",
    "mode/cam/ops",
    "mode/cam/tool",
    "mode/cam/topo",
    "mode/cam/slice",
    "mode/cam/prepare",
    "mode/cam/export",
    "mode/cam/animate",
    "mode/laser/driver",
    "kiri/widget",
    "kiri/print",
    "kiri/codec",
    "kiri/worker",
    "moto/load-stl"
].map(p => `${dir}/src/${p}.js`);

let exports_save = exports,
    module_save = module,
    THREE = {},
    geo = {},
    navigator = { userAgent: "" },
    self = {
        THREE,
        kiri: { driver: {}, loader: [] },
        location: { hostname: 'local', port: 0, protocol: 'fake' },
        postMessage: (msg) => {
            self.kiri.client.onmessage({data:msg});
        }
    };

// fake fetch for worker to get wasm, if needed
let fetch = function(url) {
    if(conf.verbose)
       console.log({fake_fetch: url});
    let fn = url;
    if(!fn.match(/^\//) && !fs.existsSync(fn))
      fn = dir + '/' + url;
    if(!fs.existsSync(fn))
       console.log(`ERR: fetch: ${fn} not found, ignored`);
    let buf = fs.readFileSync(fn);
    return new Promise((resolve, reject) => {
        resolve(new Promise((resolve, reject) => {
            resolve({
                arrayBuffer: function() {
                    return buf;
                }
            });
        }));
    });
};

class Worker {
    constructor(url) {
        if(conf.verbose)
           console.log({fake_worker: url});
    }

    postMessage(msg) {
        setImmediate(() => {
            self.kiri.worker.onmessage({data:msg});
        });
    }

    onmessage(msg) {
        // if we end up here, something went wrong
        console.trace('worker-recv', msg);
    }

    terminate() {
        // if we end up here, something went wrong
        console.trace('worker terminate');
    }
}

// node is missing these functions so put them in scope during eval
function atob(a) {
    return Buffer.from(a).toString('base64');
}

function btoa(b) {
    return Buffer.from(b, 'base64').toString();
}

backup = console.log
console.log = function() { }        // mute kiri startup messages

for (let file of files) {
    let isPNG = file.indexOf("/pngjs") > 0;
    let isClip = file.indexOf("/clip") > 0;
    let isEarcut = file.indexOf("/earcut") > 0;
    let isTHREE = file.indexOf("/three") > 0;
    if (isTHREE) {
        // THREE.js kung-fu fake-out
        exports = {};
    }
    let swapMod = isEarcut;
    if (swapMod) {
        module = { exports: {} };
    }
    let clearMod = isPNG || isClip;
    if (clearMod) {
        module = undefined;
    }
    let fn = file;
    if(!fs.existsSync(fn))
        if(conf.verbose) 
           console.log(`WARN: startup: ${fn} not found, ignored`);
    try {
        if(conf.verbose>3) 
            console.log(`INF: process ${fn} ...`);
        eval(fs.readFileSync(fn).toString());
    } catch (e) {
        //console.log({dir, file, e});
        if(conf.verbose)
            console.log({dir, file, e});
    }
    if (isClip) {
        ClipperLib = self.ClipperLib;
    }
    if (isTHREE) {
        Object.assign(THREE, exports);
        // restore exports after faking out THREE.js
        exports = exports_save;
    }
    if (isEarcut) {
        self.earcut = module.exports;
    }
    if (clearMod || swapMod) {
        module = module_save;
    }
}

console.log = backup

let kiri = self.kiri;
let moto = self.moto;
let engine = kiri.newEngine();

if(conf.version) {
   console.log(`${APPNAME} ${VERSION} (Kiri:Moto ${kiri.version}, node ${process.version})`);
   process.exit(0);
}

if(fss.length == 0 || conf.help)
   help()

console.log(`== ${APPNAME} ${VERSION} (Kiri:Moto ${kiri.version}, node ${process.version}) == https://github.com/Spiritdude/KiriMotoSlicer
   based on Kiri:Moto https://github.com/GridSpace/grid-apps`);

if(conf.verbose)
   console.log({conf:conf})

for(var i=0; i<fss.length; i++) {
   var fn = fss[i]
   var fno = fn
   fno = fno.replace(/\.\w+$/,'.gcode')
   if(conf.output)
      fno = conf.output
   if(fn != fno) 
      slice(fn,fno)
   else
      console.log(`ERR: strange filename '${fn}', must have an extension like .stl or so, skipped`);
}   
// ---------------------------------------------------------------------------------------------------------------------------

function help() {
   console.log(`${APPNAME} ${VERSION} USAGE: [<options>] [file] ...
   options:
      --help                        this usage help
      --help=<term>                 show settings matching <term>
      -h <term>                        \"       \"
      --version                     display version and exit
      --verbose                     increase verbosity
      -v or -vvv                       \"       \"
      --output=<fn>                 force output filename
      -o <fn>                          \"       \"
      --load=<conf>                 load configuration (lines of <k>=<v>, or experimental JSON as grid-apps/src/dev/*)
      -l <conf>                              \"                \"                      \"
      --setTemperature=<s>          include set temperature extruder & bed in gcodePre (default: `+JSON.stringify(conf.setTemperature)+`)
      --bedOrigin=<x>,<y>           set origin of bed (use with --outputOriginCenter=true) (default: `+JSON.stringify(conf.bedOrigin)+`)
`);

   console.log("   slice configuration:");     
   for(let k in slice_conf) {
      list_option(k,slice_conf)
   }
   console.log("");
   console.log("   device configuration:");     
   for(let k in device_conf) {
      list_option(k,device_conf)
   }
   console.log("");
   console.log(`   examples:
      ${BINNAME} cube.stl
      ${BINNAME} cube.stl -o test.gcode
      ${BINNAME} -v cube.stl --sliceHeight=0.1
      ${BINNAME} cube.stl --extruders.0.extNozzle=0.5 --sliceHeight=0.4
      ${BINNAME} -h slice
      ${BINNAME} -h .
`);   
   process.exit(-1);
}

function list_option(k,s) {
   let kn = k;
   let v = "";
   let ko = k;
   
   kn = kn.replace(/([A-Z]+)/g,function(a,b) { return b.length == 1 ? " "+b.toLowerCase() : " "+b })
   if(Array.isArray(s[k])) {
      if(typeof s[k][0] === "object") {
         v = s[k]
         ko += ".[0..<n-1>].{"
         let i = 0
         for(let ki in s[k][0]) {
            if(i++)
               ko += ","
            ko += ki
         }
         ko += "}"
      } else 
         v = s[k].join("\n")
   } else 
      v = s[k]
   v = JSON.stringify(v)
   if(conf_options[k]) 
      kn += " "+JSON.stringify(conf_options[k])
   console.log(`      --${ko}=<v>`+(" ".repeat(ko.length<24?24-ko.length:1))+`set ${kn} (default: ${v})`);
}

function load_conf(fn) {
   if(fs.existsSync(fn)) {
      if(conf.verbose)
         console.log(`INF: load configuration <${fn}>`)
      let t = fs.readFileSync(fn)
      t = t.toString()
      if(t.match(/^\s*\{/)) {        // is JSON
         d = JSON.parse(t);
         if(d['pre']) 
            conf.gcodePre = d['pre'].join("\n")
         if(d['post']) 
            conf.gcodePost = d['post'].join("\n")
         if(d['extruders']) 
            for(e in d.extruders)
               for(k in d.extruders[e])
                  conf['extruders.'+e+'.'+k] = d.extruders[e][k]
         if(d['settings'])
            for(e in d.settings)     // object
               conf[e] = d.settings[e]
         if(d['profiles'])    
            for(p of d.profiles)     // array
               for(k in p)
                  conf[k] = p[k]
         // FIXME: support 'cmd' key as well
      } else {                       // is line-wise <k> = <v>
         let ln = 0;
         for(l of t.split(/\n/)) {
            let m = l.match(/^\s*#/)
            ln++
            if(m) 
               continue
            if(conf.verbose>1)
               console.log("INF:",fn,">>",l)
            m = l.match(/^\s*(\w+)\s*=\s*(\S.*)/)
            if(m && m[1] && m[2]) {
               conf[m[1]] = m[2]
               if(conf.verbose) 
                  console.log(`INF: ${fn}: ${m[1]} = ${m[2]}`)
            } else if(l.match(/\S/))
               console.log(`WARN: ${fn} #${ln}: could not parse '${l}', skipped`)
         }
      }
   } else
      console.log(`WARN: configuration ${fn} is not accessible, disregarded`)
}

function slice(fn,fno) {
   let start = new Date();

   if(!fs.existsSync(fn)) {
      console.log(`ERR: '${fn}' is not accessible, skipped`)
      return
   }
   if(conf.verbose)
      console.log(`INF: slicing ${fn} to ${fno}`)
   else 
      console.log(`${APPNAME}: slicing ${fn} ...`)
   if(conf.verbose>1)
      console.log({slice: slice_conf, device: device_conf})
   fetch(fn).then(data => {
       let buf = data.arrayBuffer().buffer;
       engine.parse(buf)
       .then(data => {
           if(conf.verbose)
              console.log({loaded: data});
       })
       .then(() => engine.moveTo(conf.bedOrigin[0],conf.bedOrigin[1],0))
       .then(() => engine.setProcess(slice_conf))
       .then(() => engine.setDevice(device_conf))
       .then(eng => eng.slice())
       .then(eng => eng.prepare())
       .then(eng => engine.export())
       .then(gcode => {
           if(conf.verbose>2)
              console.log({gcode})
           if(gcode && gcode.length > 0) {
              console.log(`${APPNAME}:    => ${fno} (took `+parseInt((new Date()-start)/1000)+" secs)")
              let fl = gcode.match(/filament used: (\d+)/)
              if(fl && fl[1]) {
                 let n = parseFloat(fl[1])/1000
                 //console.log(sprintf(`${APPNAME}:       %d.%dm filament used`,parseInt(Math.floor(n)),(n*10)%10))
                 console.log(sprintf(`${APPNAME}:       %.3fm filament used`,n))
              }
              fs.writeFile(fno,gcode,(err) => {})
           } else {
              console.log(`${APPNAME}: ERR: failed to create any gcode`)
              process.exit(-1)
           }
       })
       .catch(error => {
           console.log({error})
       });
   });
}

function dot(s,k,v) {                  // how many times did I implement this? too many times...
   let p = k.split(".")
   let ks = p.pop()
   let m = typeof v !== "undefined" ? "w" : "r"
   
   for(let q of p) {
      if(typeof s === "undefined") {            // deep doesn't exist?
         if(m=="w")                             // writing?
            s = []
         else                                   // reading return undefined
            return undefined;
      }
      if(q.match(/^\d+$/))
         q = parseInt(q)
      s = s[q]
      if(conf.verbose>2)
         console.log("deep",q,typeof q)
   }
   if(ks.match(/^\d+$/))
      ks = parseInt(ks)
   if(m=="w") {                                 // write
      let t = s && typeof s[ks];                // let's keep the same type if possible
      if(t=="number")
         v = parseFloat(v);
      if(typeof s === "undefined")              // still not created?
         s = []
      s[ks] = v
      if(conf.verbose>2)
         console.log("writing",ks,v)
   } else                                       // read
      return s ? s[ks] : undefined;
}

/**
 * sprintf() for JavaScript v.0.4
 *
 * Copyright (c) 2007 Alexandru Marasteanu <http://alexei.417.ro/>
 * Thanks to David Baird (unit test and patch).
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 */

function str_repeat(i, m) { for (var o = []; m > 0; o[--m] = i); return(o.join('')); }

function sprintf () {
  var i = 0, a, f = arguments[i++], o = [], m, p, c, x;
  while (f) {
    if (m = /^[^\x25]+/.exec(f)) o.push(m[0]);
    else if (m = /^\x25{2}/.exec(f)) o.push('%');
    else if (m = /^\x25(?:(\d+)\$)?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(f)) {
      if (((a = arguments[m[1] || i++]) == null) || (a == undefined)) throw("Too few arguments.");
      if (/[^s]/.test(m[7]) && (typeof(a) != 'number'))
        throw("Expecting number but found " + typeof(a));
      switch (m[7]) {
        case 'b': a = a.toString(2); break;
        case 'c': a = String.fromCharCode(a); break;
        case 'd': a = parseInt(a); break;
        case 'e': a = m[6] ? a.toExponential(m[6]) : a.toExponential(); break;
        case 'f': a = m[6] ? parseFloat(a).toFixed(m[6]) : parseFloat(a); break;
        case 'o': a = a.toString(8); break;
        case 's': a = ((a = String(a)) && m[6] ? a.substring(0, m[6]) : a); break;
        case 'u': a = Math.abs(a); break;
        case 'x': a = a.toString(16); break;
        case 'X': a = a.toString(16).toUpperCase(); break;
      }
      a = (/[def]/.test(m[7]) && m[2] && a > 0 ? '+' + a : a);
      c = m[3] ? m[3] == '0' ? '0' : m[3].charAt(1) : ' ';
      x = m[5] - String(a).length;
      p = m[5] ? str_repeat(c, x) : '';
      o.push(m[4] ? a + p : p + a);
    }
    else throw ("Huh ?!");
    f = f.substring(m[0].length);
  }
  return o.join('');
}
